#!/usr/bin/env python3
#
# Simple PWM controller
# Remote control tool
#
# Copyright (c) 2018-2021 Michael Buesch <m@bues.ch>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

from fractions import Fraction
from libsimplepwm import *
from libsimplepwm.util import *
import argparse
import pathlib
import sys
import time

def parse_setpoints(string):
    def parseOne(v):
        try:
            v = v.strip()
            if v.casefold().startswith("0x".casefold()):
                # Raw 16 bit hex value.
                v = int(v, 16)
                if not 0 <= v <= 0xFFFF:
                    raise SimplePWMError("Setpoint raw value "
                        "out of range 0-0xFFFF.")
                return v
            if v.endswith("%"):
                # Percentage
                v = float(v[:-1])
                if not 0.0 <= v <= 100.0:
                    raise SimplePWMError("Setpoint percentage "
                        "out of range 0%-100%.")
                return round(v * 0xFFFF / 100.0)
            if v.endswith("*"):
                # Degrees (0-360)
                v = float(v[:-1])
                return round((v % 360.0) * 0xFFFF / 360.0)
            # Value 0-255
            v = int(v)
            if 0 <= v <= 0xFF:
                return (v << 8) | v
            raise ValueError
        except ValueError as e:
            raise SimplePWMError("Setpoint value parse error.")
    s = string.split(",")
    if len(s) == 1:
        return [ parseOne(s[0]) for i in range(3) ]
    if len(s) != 3:
        raise SimplePWMError("Setpoints are not a comma separated triple.")
    return [ parseOne(v) for v in s ]

def parse_pwmcorrindex(string):
    try:
        index = int(string)
        if not 0 <= index <= 3:
            raise SimplePWMError("PWM correction index is out of range.")
        indices = range(3) if index == 0 else (index-1,)
        return indices
    except ValueError as e:
        raise SimplePWMError("PWM correction parameter is invalid.")

def parse_pwmcorr(string):
    s = string.split(":")
    if len(s) != 2:
        raise SimplePWMError("PWM correction parameter is invalid.")
    try:
        indices = parse_pwmcorrindex(s[0])
        fraction = Fraction(float(s[1])).limit_denominator(0xFFFF)
        if not 0 <= fraction.numerator <= 0xFFFF:
            raise SimplePWMError("PWM correction factor is out of range.")
        return indices, fraction
    except (ValueError, ZeroDivisionError) as e:
        raise SimplePWMError("PWM correction parameter is invalid.")

class ArgumentParserOrderedNamespace(argparse.Namespace):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        super().__setattr__("_setupDone", False)
        super().__setattr__("_orderedArgs", [])

    def __setattr__(self, name, value):
        sanitizedName = name.replace("_", "-")
        if (not self._setupDone and
            sanitizedName in (n for n, v in self._orderedArgs)):
            super().__setattr__("_setupDone", True)
            del self._orderedArgs[:]
        self._orderedArgs.append( (sanitizedName, value) )
        super().__setattr__(name, value)

    @property
    def orderedArgs(self):
        return self._orderedArgs if self._setupDone else ()

def main():
    s = None
    try:
        p = argparse.ArgumentParser(description="SimplePWM remote control")
        p.add_argument("-p", "--ping", action="store_true",
                       help="Send a ping to the device and wait for pong reply.")
        p.add_argument("-b", "--get-battery", action="store_true",
                       help="Get the battery voltage.")
        p.add_argument("-e", "--get-eeprom-enable", action="store_true",
                       help="Get the state of the EEPROM.")
        p.add_argument("-E", "--eeprom-enable", type=int, default=None,
                       metavar="BOOL",
                       help="Enable/disable the EEPROM.")
        p.add_argument("-a", "--get-analog", action="store_true",
                       help="Get the state of the analog inputs.")
        p.add_argument("-A", "--analog", type=int, default=None,
                       metavar="BOOL",
                       help="Enable/disable the analog inputs.")
        p.add_argument("-r", "--get-rgb", action="store_true",
                       help="Get the current RGB setpoint values.")
        p.add_argument("-R", "--rgb", type=parse_setpoints, default=None,
                       metavar="R,G,B",
                       help="Set the RGB setpoint values.")
        p.add_argument("-s", "--get-hsl", action="store_true",
                       help="Get the current HSL setpoint values.")
        p.add_argument("-S", "--hsl", type=parse_setpoints, default=None,
                       metavar="H,S,L",
                       help="Set the HSL setpoint values.")
        p.add_argument("-c", "--get-pwm-corr", type=parse_pwmcorrindex,
                       metavar="INDEX",
                       help="Get PWM lower range correction factor. "
                            "Index is the PWM index (1-3) or 0 for all.")
        p.add_argument("-C", "--pwm-corr", type=parse_pwmcorr, default=None,
                       metavar="INDEX:FACTOR",
                       help="Set PWM lower range correction factor.")
        p.add_argument("-W", "--wait", type=float,
                       metavar="SECONDS",
                       help="Delay for a fractional number of seconds.")
        p.add_argument("-L", "--loop", action="store_true",
                       help="Repeat the whole sequence.")
        p.add_argument("-d", "--debug-device", action="store_true",
                       help="Read and dump debug messages from device.")
        p.add_argument("-D", "--debug", action="store_true",
                       help="Enable remote side debugging.")
        p.add_argument("-F", "--write-flash", type=pathlib.Path,
                       metavar="HEXFILE",
                       help="Write an application flash hex.")
        p.add_argument("-P", "--write-eeprom", type=pathlib.Path,
                       metavar="HEXFILE",
                       help="Write an application EEPROM hex.")
        p.add_argument("port", nargs="?", type=pathlib.Path,
                       default=pathlib.Path("/dev/ttyUSB0"),
                       help="Serial port.")
        args = p.parse_args(namespace=ArgumentParserOrderedNamespace())

        if args.debug:
            printDebug.enabled = True
        if args.debug_device:
            printDebugDevice.enabled = True

        s = SimplePWM(port=str(args.port),
                      dumpDebugStream=args.debug_device,
                      dumpDataStream=False,
                      recoveryMode=(args.write_flash or args.write_eeprom))

        repeat = True
        while repeat:
            for name, value in args.orderedArgs:
                if name == "write-flash":
                    s.writeFlash(value)
                if name == "write-eeprom":
                    s.writeEeprom(value)
                if name == "ping":
                    if value:
                        s.ping()
                if name == "get-battery":
                    if value:
                        meas, drop = s.getBatVoltage()
                        print(f"Battery: "
                              f"measured {meas/1000:.2f} V, "
                              f"drop {drop/1000:.2f} V, "
                              f"actual {(meas+drop)/1000:.2f} V")
                if name == "eeprom-enable":
                    s.setEepromEn(value)
                if name == "get-eeprom-enable":
                    if value:
                        enabled = "enabled" if s.getEepromEn() else "disabled"
                        print(f"EEPROM: {enabled}")
                if name == "analog":
                    s.setAnalogEn(value)
                if name == "get-analog":
                    if value:
                        enabled = "enabled" if s.getAnalogEn() else "disabled"
                        print(f"Analog inputs state: "
                              f"{enabled}")
                if name == "rgb":
                    s.setRGB(value)
                if name == "get-rgb":
                    if value:
                        rgb = s.getRGB()
                        print(f"RGB setpoints: "
                              f"{rgb[0]*100/0xFFFF:.1f}%, "
                              f"{rgb[1]*100/0xFFFF:.1f}%, "
                              f"{rgb[2]*100/0xFFFF:.1f}%")
                if name == "hsl":
                    s.setHSL(value)
                if name == "get-hsl":
                    if value:
                        hsl = s.getHSL()
                        print(f"HSL setpoints: "
                              f"{hsl[0]*100/0xFFFF:.1f}%, "
                              f"{hsl[1]*100/0xFFFF:.1f}%, "
                              f"{hsl[2]*100/0xFFFF:.1f}%")
                if name == "pwm-corr":
                    indices, fraction = value
                    for i in indices:
                        s.setPwmCorr(i, fraction)
                if name == "get-pwm-corr":
                    indices = value
                    for i in indices:
                        fraction = s.getPwmCorr(i)
                        print(f"PWM {i+1} "
                              f"lower range correction factor: "
                              f"{float(fraction)}")
                if name == "wait":
                    time.sleep(value)
                if name == "loop":
                    repeat = True
                    break
            else:
                repeat = False
        if args.debug_device:
            s.dumpDebugStream()
        s.disconnect()
    except SimplePWMError as e:
        if s:
            s.disconnect()
        printError(e)
        return 1
    except KeyboardInterrupt as e:
        if s:
            s.disconnect()
        printInfo("Interrupted.")
        return 1
    return 0

if __name__ == "__main__":
    sys.exit(main())

# vim: ts=4 sw=4 expandtab
